热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux|多线程与fork

经由fork()创建的子进程,其中只有一个线程。子进程里仅存的线程,对应着主进程里调用fork()函数的线程。此外,主进程的整个虚存空间都

经由 fork() 创建的子进程,其中只有一个线程。子进程里仅存的线程,对应着主进程里调用 fork() 函数的线程。此外,主进程的整个虚存空间都被复制到了子进程。因而,包括互斥锁、条件变量、其余线程的内部对象等,都保持着原样。由此引发的问题,可以考虑用 pthread_atfork() 函数解决。
参考自:https://liam.page/2017/01/17/fork-safe/

1.线程内fork产生的子进程,只会执行该线程部分(主进程不会向下执行)

#include
#include
#include
#include
#include
#include
/* 功能说明:* 主函数 ---- ------main:主线程---> 打印:main:主线程id=..* 工作线程 --fork() ---fun:父进程---> 打印: fun:父进程id=.. * |---fun:子进程---> 打印: fun: 子进程id=..*/void *Thread_fun()
{pid_t pid &#61; fork();if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}}else{for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}}
}int main()
{pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);exit(0);
}

在这里插入图片描述
fork产生的子进程中只有一个线程。

执行结果&#xff1a;
main::主线程 【1】:id &#61; 74226
fun::父进程 【1】:id &#61; 74226
fun::子进程 【1】:id &#61; 74228
fun::父进程 【2】:id &#61; 74226
main::主线程 【2】:id &#61; 74226
fun::子进程 【2】:id &#61; 74228
fun::父进程 【3】:id &#61; 74226
main::主线程 【3】:id &#61; 74226
fun::子进程 【3】:id &#61; 74228
main is stop …

2.尝试加锁控制执行状态

如果我们把输出屏幕看成一块共享资源的话&#xff0c;我们想要输出结果为&#xff0c;先让main中的打印完成&#xff0c;在打印线程内的。则我们需要加锁完成。

2.1 fork复制了main中锁状态&#xff0c;则线程内的子进程部分加锁。效果不理想

把临界区代码加锁。&#xff08;需要注意的是&#xff0c;fork会复制父进程加锁状态&#xff09;&#xff0c;如果我们代码这样写。

include <stdio.h>
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁
/* 功能说明&#xff1a;* 主函数 ---- ------main:主线程---> 打印&#xff1a;main&#xff1a;主线程id&#61;..* 工作线程 --fork() ---fun&#xff1a;父进程---> 打印&#xff1a; fun&#xff1a;父进程id&#61;.. * |---fun&#xff1a;子进程---> 打印&#xff1a; fun: 子进程id&#61;..** 加锁&#xff1a;保证先让main中的打印先完成*/void *Thread_fun()
{sleep(1); // 保证让主进程先获得锁pid_t pid &#61; fork();if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}else{// 注意&#xff1a;这里没加锁&#xff0c;因为子进程复制的父进程此时是处于加锁状态的&#xff0c;如果这里加锁会产生死锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【f_c%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证主线程先执行输出操作for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【m %d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);exit(0);
}

执行结果&#xff1a;
main::主线程 【m 1】:id &#61; 75080
main::主线程 【m 2】:id &#61; 75080
fun::子进程 【f_c1】:id &#61; 75082
main::主线程 【m 3】:id &#61; 75080
fun::子进程 【f_c2】:id &#61; 75082
fun::子进程 【f_c3】:id &#61; 75082
main is stop …
fun::父进程 【f_f1】:id &#61; 75080
fun::父进程 【f_f2】:id &#61; 75080
fun::父进程 【f_f3】:id &#61; 75080

可以看到&#xff0c;在父进程中&#xff0c;遵循先答应main的内容&#xff0c;在打印线程函数。&#xff08;main is stop … 之后才执行fun::父进程&#xff09;。但是由于子进程中临界区没有加锁&#xff0c;子进程的输出不受限制&#xff0c;干扰了主进程中的main的输出。

2.1 fork复制了main中锁状态&#xff0c;则线程内的子进程部分加锁。产生死锁&#xff0c;子进程一直阻塞

如果我们给子进程中的临界区加锁又会产生另一种问题。&#xff08;死锁&#xff09;以下代码仅修改了子进程的临界区进行加锁操作。

#include
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁
/* 功能说明&#xff1a;* 主函数 ---- ------main:主线程---> 打印&#xff1a;main&#xff1a;主线程id&#61;..* 工作线程 --fork() ---fun&#xff1a;父进程---> 打印&#xff1a; fun&#xff1a;父进程id&#61;.. * |---fun&#xff1a;子进程---> 打印&#xff1a; fun: 子进程id&#61;..** 在工作线程中fork&#xff0c;产生的子进程&#xff0c;只会执行工作线程内的代码。但子进程会复制父进程的状态&#xff0c;如加锁。*** 加锁&#xff1a;保证先让main中的打印先完成* 问题&#xff1a;fork会复制父进程中的加锁状态&#xff0c;即在工作线程中&#xff0c;子进程已经是处于加锁状态的&#xff0c;再次加锁无法完成&#xff0c;则产生死锁。*/// 注意&#xff1a;以下代码子进程会产生死锁
void *Thread_fun()
{sleep(1); // 保证让主进程先获得锁pid_t pid &#61; fork();if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}else{/* 这里产生死锁&#xff0c;复制的父进程中加锁状态,* 但是线程内使用fork不会再执行父进程中的代码&#xff0c;因此..*/pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【f_c%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证主线程先执行输出操作for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【m %d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);exit(0);
}

输出结果&#xff1a;
main::主线程 【m 1】:id &#61; 75247
main::主线程 【m 2】:id &#61; 75247
main::主线程 【m 3】:id &#61; 75247
main is stop …
fun::父进程 【f_f1】:id &#61; 75247
fun::父进程 【f_f2】:id &#61; 75247
fun::父进程 【f_f3】:id &#61; 75247

2.3.分析阻塞原因

可以看到子进程中&#xff0c;并没有执行打印操作。使用ps查看&#xff0c;发现有进程还未退出&#xff0c;正是我们的子进程。
在这里插入图片描述
父进程执行过程大致如下&#xff1a;
在这里插入图片描述
子进程中&#xff0c;因为工作线程以外的部分不继续执行了&#xff0c;则工作线程一直处于阻塞状态。
在这里插入图片描述

2.4 对fork加锁&#xff08;试探锁&#xff0c;试探main中是否已经解锁&#xff09;

解决方案&#xff1a;
确保fork()时&#xff0c;父进程是没有加锁的。因为本程序需要实现先打印main的内容&#xff0c;因此我们可以这样做&#x1f447;&#xff0c;确保main中先打印并且不会占用占用锁资源。

// 尝试加锁&#xff0c;如果可以加锁&#xff0c;则证明父进程没有占用锁资源
pid_t pid &#61; fork();
// 解锁

代码如下&#xff1a;

#include
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁void *Thread_fun()
{pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证临界区锁以释放&#xff0c;不会被fork复制到子进程中阻塞临界区pid_t pid &#61; fork();pthread_mutex_unlock(&mutex); // 解锁if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}else{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【f_c%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证主线程先执行输出操作for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【m %d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);exit(0);
}

输出结果&#xff1a;
main::主线程 【m 1】:id &#61; 75807
main::主线程 【m 2】:id &#61; 75807
main::主线程 【m 3】:id &#61; 75807
main is stop …
fun::父进程 【f_f1】:id &#61; 75807
fun::子进程 【f_c1】:id &#61; 75811
fun::子进程 【f_c2】:id &#61; 75811
fun::父进程 【f_f2】:id &#61; 75807
fun::子进程 【f_c3】:id &#61; 75811
fun::父进程 【f_f3】:id &#61; 75807

3.系统提供的解决方案 pthread_atfork

如果我们有很多不同的锁&#xff0c;按照我们之前的解决方案&#xff0c;我们需要在 fork 之前进行 n 多次的试探动作&#xff0c;不仅麻烦还极容易引起死锁。而系统为我们提供了一个函数可以解决这个问题。

#include int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));#include
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁
pthread_mutex_t lock; // 创建互斥锁void prepare() {pthread_mutex_lock(&mutex); // 加锁printf("prepare: 加锁 ...\n");
}void parent() {pthread_mutex_unlock(&mutex);printf("parent : 父进程解锁 ...\n");
}void child() {pthread_mutex_unlock(&mutex);printf("child : 子进程解锁 ...\n");
}void *Thread_fun(void*arg)
{sleep(1);pthread_mutex_lock(&lock); // 加锁for(int i &#61; 0; i < 5; i&#43;&#43;){printf(" fun::%s 【f_f%d】:id &#61; %d\n",(char*)arg,i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&lock); // 解锁
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_mutex_init(&lock,NULL);pthread_t id; // 创建线程并调用工作线程// 注册forkint res &#61; pthread_atfork(prepare, parent, child);assert(res &#61;&#61; 0);char buff[] &#61; "父进程";int ret &#61; pthread_create(&id,NULL,Thread_fun,(void*)buff);assert(ret &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功/* fork调用前调用preoarefork调用完&#xff0c;在调用child和parent*/pid_t pid &#61; fork();/*在父进程中&#xff08;当前进程中&#xff09;调用fork。经由 fork() 创建的子进程&#xff0c;其中只有一个线程&#xff08;调用fork的线程&#xff0c;即主线程&#xff09;因此&#xff0c;fork产生的子进程中&#xff0c;不会执行Thread_fun()函数&#xff0c;则我们可以在父进程&#xff08;当前进程中手动调用&#xff09;*/if(pid &#61;&#61; 0) // 子进程{sleep(1); // 保证main中父进程先执行// 注&#xff1a;这里的子进程与父进程是两个进程&#xff0c;不共享锁。同步需要设置管道或者信号量等 // ********strcpy(buff,"子进程");Thread_fun((void*)buff);}else // 父进程{pthread_mutex_lock(&lock); // 加锁for(int i &#61; 0; i < 10; i&#43;&#43;){printf("main::主函数 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&lock); // 解锁}// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);pthread_mutex_destroy(&lock);exit(0);
}

输出&#xff1a;
父进程&#xff1a;先输出 main中的&#xff0c;再输出工作线程中的
子进程&#xff1a;只有一个线程&#xff0c;在main中&#xff0c;通过手动调用执行工作函数。由于进程间全局变量&#xff08;我们定义的锁&#xff09;不共享&#xff0c;所以子进程在输出时不受锁影响。如有需要加入型号量等。。


推荐阅读
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • Linux 中使用 clone 函数来创建线程
    2019独角兽企业重金招聘Python工程师标准Linux上创建线程一般使用的是pthread库实际上libc也给我们提供了创建线程的函数那就是cloneintclone(i ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • Linux线程的同步和互斥
    目录1、线程的互斥2、可重入VS线程安全3、线程的同步1、线程的互斥 ... [详细]
  • 作者一直强调的一个概念叫做oneloopperthread,撇开多线程不谈,本篇博文将学习,怎么将传统的IO复用pollepoll封装到C++类中。1.IO复用复习使用p ... [详细]
  • 不知道你是否还记得之前在进程中的信号处理时,提到过阻塞信号集与未决信号集的概念,如果你已经忘记了,请参考《阻塞信号与未决信号》一文回忆一下 ... [详细]
  • C语言编程gcc怎么生成静态库.a和动态库.so
    这篇文章将为大家详细讲解有关C语言编程gcc怎么生成静态库.a和动态库.so,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章 ... [详细]
  • 主要用的线程函数:1.创建线程:12intpthread_create(pthread_t*thread,constpthread_attr_ ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 线程漫谈——线程基础
    本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。进程与线程理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必 ... [详细]
  • 我首先提高本程序的权限,然后成功得到服务程序(exe)的读写权限,开辟新的远程内存空间,然后拷贝程序执行的代码,但写入我的dll后,服务程序什么反应也没有。 ... [详细]
author-avatar
手机用户2502885897
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有